import re

from django.db import models
from django.contrib.auth.models import User

from vcs import backends


REPOSITORY_TYPES = ((k, v['name']) for k, v in backends.backend_map.items())

_tagsep = re.compile(r'[.\-_]')

def cmp_tags(a, b):
    """
    Allows for special comparison of two (string) tag names, such as 1.0.5 or
    2.5.2-beta3. Python string comparison would get us 99% of the way 
    there, except for the -beta3 type stuff, where (almost all the time, in
    our experience) something with a -textual suffix should be "less than" the
    straight numerical version (i.e. a prerelease vs a release).
    """
    a_parts = _tagsep.split(a)
    b_parts = _tagsep.split(b)
    # Can't assume 2.6 itertools.izip_longest
    a_shorter = len(b_parts) - len(a_parts)
    b_shorter = len(a_parts) - len(b_parts)
    a_parts.extend([''] * a_shorter)
    b_parts.extend([''] * b_shorter)

    zipped = zip(a_parts, b_parts)
    result = 0
    for i, (aN, bN) in enumerate(zipped):
        # Handle text suffixes - if we're at the "end" (meaning up till now,
        # both have been identical) and we're comparing empty string to text,
        # then b is actually less than a.
        if i == len(zipped) and aN == '' and not bN.isdigit():
            result = 1
        # Otherwise, employ normal Python string comparison
        else:
            result = cmp(aN, bN)
        # Since we're moving in order from left to right, return the very first
        # non-equal result (i.e. 2.*.* is always greater than 1.*.*)
        if result != 0:
            break
    return result


class Repository(models.Model):
    """
    A repository is a version control system installation where code is stored.
    In Django-VCS they are at the top of the model hierarchy.
    """
    # Which backend to use (may also be used to determine what behavior to
    # exhibit in e.g. templates)
    repository_type = models.CharField(max_length=3, choices=REPOSITORY_TYPES,
        blank=False)
    # Name, unique across entire site
    name = models.CharField(max_length=100, unique=True)
    # Repository URL, e.g. git://foo.org/bar.git or https://svn.mysite.com/foo
    url = models.CharField(max_length=250)
    # Auth username
    username = models.CharField(blank=True, max_length=100)
    # Auth password
    password = models.CharField(blank=True, max_length=100)

    # Optional fields for specifying where to find tags/branches (for e.g. SVN)
    branch_path = models.CharField(blank=True, max_length=255)
    tag_path = models.CharField(blank=True, max_length=255)
    
    class Meta:
        ordering = ('name',)
        verbose_name_plural = 'repositories'

    def __unicode__(self):
        return self.name

    @models.permalink
    def get_absolute_url(self):
        return ('vcs-repository-detail', (), {'repository': self.name}) 
    
    def refresh(self):
        """
        Causes this repository to refresh, adding all of the latest commits.
        """
        backend = backends.get_backend(self.repository_type)
        backend.refresh(self)
    
    @property
    def branch_names(self):
        return [x.name for x in self.branches.all()]

    @property
    def active_branches(self):
        return self.branches.filter(removed=False)

    @property
    def inactive_branches(self):
        return self.branches.filter(removed=True)

    @property
    def commits(self):
        return Commit.objects.filter(
            branches__repository__pk=self.id
        ).distinct()

    @property
    def tags(self):
        return Tag.objects.filter(branch__repository__pk=self.id).distinct()
    
    def ordered_tags(self):
        return sorted(list(self.tags), cmp=lambda a,b: cmp_tags(a.name, b.name))


class Branch(models.Model):
    """
    A specific collection of commits, usually a single line of development.
    """
    # Parent repository
    repository = models.ForeignKey(Repository, related_name='branches')
    # Name (unique per repository)
    name = models.CharField(max_length=100)
    # Path to branch (for systems like SVN)
    path = models.CharField(max_length=100, blank=True, null=True)
    # Optional description
    description = models.CharField(max_length=250)
    # Old branches (not reachable in current HEAD, or which existed earlier on
    # and are no longer found) are marked as such.
    removed = models.BooleanField(default=False)

    class Meta:
        unique_together = ('repository', 'name')
        ordering = ('repository', 'name')
        verbose_name_plural = 'branches'

    def __unicode__(self):
        return self.name

    @models.permalink
    def get_absolute_url(self):
        return ('vcs-branch-detail', (), {
            'repository': self.repository.name,
            'branch': self.name
        }) 

    @property
    def url(self):
        """
        Mostly for use with SVN or other VCSs where a "branch" is just a path
        in the repository.
        """
        return self.repository.url + '/' + self.path

    @property
    def head(self):
        if self.commits.count():
            return self.commits.get(is_head=True)


class Tag(models.Model):
    """
    A snapshot of one specific branch at one point in time. Typically used
    for releases or other special occasions/versions of the code.
    """
    # Branch this tag was cut from
    branch = models.ForeignKey(Branch, related_name="tags")
    # The name of the tag (e.g. 1.0.5)
    name = models.CharField(max_length=255)
    # Whether or not the tag has been removed (mostly for SVN)
    removed = models.BooleanField(default=False)
    # Optional description
    description = models.TextField(blank=True)

    class Meta:
        ordering = ('timestamp',)

    @property
    def repository(self):
        return self.branch.repository


class Commit(models.Model):
    """
    A commit is a new set of patches or snapshot of the repository at a given
    point in time. Commits typically belong to a single Branch, but may belong
    to more than one in the case of a merge commit.
    """
    # Which branch(es) this commit belongs to
    branches = models.ManyToManyField(Branch, related_name='commits')
    # Whether this commit represents the HEAD of its branch(es)
    is_head = models.BooleanField(default=False)
    # The commit(s) which this commit was generated from, representing the prior
    # patchset or state of the repository.
    parents = models.ManyToManyField('self', symmetrical=False,
        related_name='children')
    # Unique identifier, such as '122' for SVN or a hash for Git.
    identifier = models.CharField(max_length=100)
    # User who checked in this commit to the repository.
    committer = models.ForeignKey(User, related_name='commits_committed')
    # User who wrote the changes in this commit (often the same as committer)
    author = models.ForeignKey(User, related_name='commits_authored')
    # Commit message.
    message = models.TextField(blank=True)
    # Timestamp of checkin
    commit_timestamp = models.DateTimeField()
    # Timestamp of authorship (when author checked into local repo, or same as
    # commit for e.g. SVN.
    author_timestamp = models.DateTimeField()
    
    class Meta:
        ordering = ('-commit_timestamp',)

    def __unicode__(self):
        return self.identifier
    
    @property
    def default_branch(self):
        """
        As commits have no direct relationship to the repository, they must
        piggyback on their branch(es). Commits should not exist without a
        branch, so grabbing the first one should be safe.
        """
        return self.branches.all()[0]

    @property
    def repository(self):
        return self.default_branch.repository

    @models.permalink
    def get_absolute_url(self):
        return ('vcs-commit-detail', (), {
            'repository': self.repository.name,
            'branch': self.default_branch.name,
            'commit': self.identifier
        }) 

    @property
    def parent(self):
        """
        Attempts to return a single parent of this commit. If the commit has
        more than one parent, an error is raised. If it has no parents, None
        is returned.
        """
        c = self.parents.count()
        if c == 1:
            return self.parents.all()[0]
        elif c == 0:
            return None
        else:
            raise Exception, "More than one parent commit found!"


MODIFICATION_TYPES = (
    ('A', 'Added'),
    ('M', 'Modified'),
    ('R', 'Removed')
)

class FileModification(models.Model):
    """
    A file added, modified or removed in a commit.
    """
    # FileModifications belong to a single commit
    commit = models.ForeignKey(Commit, related_name='files')
    # Path of modified file, relative to project root.
    filepath = models.CharField(max_length=300)
    # Was this an addition, modification, removal, etc?
    modification_type = models.CharField(max_length=1,
        choices=MODIFICATION_TYPES)
    # Patch/diff of modification, if applicable.
    diff = models.TextField(blank=True)

    def __unicode__(self):
        return self.filepath
    
    class Meta:
        unique_together = ('commit', 'filepath')
        ordering = ('filepath',)
